package org.ezplatform.office.rd.common;

import org.apache.tools.zip.ZipEntry;
import org.apache.tools.zip.ZipOutputStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

/**
 * ZipUtils - 安全增强版
 * @author 	ZENG.XIAO.YAN
 * @date 	2017年11月19日 下午7:16:08
 * @version v1.0
 */
public class ZipUtils {

    private static final Logger logger = LoggerFactory.getLogger(ZipUtils.class);
    private static final int BUFFER_SIZE = 2 * 1024;

    // 安全常量定义
    private static final int MAX_PATH_LENGTH = 500;
    private static final int MAX_FILENAME_LENGTH = 255;
    private static final long MAX_FILE_SIZE = 100 * 1024 * 1024; // 100MB
    private static final int MAX_EXTRACT_COUNT = 1000; // 最大解压文件数

    // 安全模式定义
    private static final Pattern SAFE_PATH_PATTERN = Pattern.compile("^[a-zA-Z0-9_\\-/\\\\.\\u4e00-\\u9fa5]+$");
    // private static final Pattern SAFE_FILENAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_\\-\\.\\u4e00-\\u9fa5\\(\\)（）]+$");
    private static final Pattern SAFE_FILENAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_\\-\\.\\u4e00-\\u9fff\\(\\)（）]+$");

    private static final String[] DANGEROUS_PATH_PATTERNS = {"..", "//", "\\\\", "<", ">", "|", ":", "*", "?"};

    /**
     * 验证和清理文件路径
     */
    private static String validateAndSanitizePath(String path) {
        if (path == null || path.trim().isEmpty()) {
            throw new IllegalArgumentException("文件路径不能为空");
        }

        // 长度检查
        if (path.length() > MAX_PATH_LENGTH) {
            throw new IllegalArgumentException("文件路径过长: " + path.length());
        }

        // 检查危险模式
        for (String dangerous : DANGEROUS_PATH_PATTERNS) {
            if (path.contains(dangerous)) {
                throw new IllegalArgumentException("文件路径包含不安全字符: " + dangerous);
            }
        }

        // 使用Java标准库规范化路径
        try {
            Path normalizedPath = Paths.get(path).normalize();
            String pathStr = normalizedPath.toString();

            // 检查路径格式
            if (!SAFE_PATH_PATTERN.matcher(pathStr).matches()) {
                throw new IllegalArgumentException("文件路径包含不安全字符");
            }

            return pathStr;
        } catch (Exception e) {
            throw new IllegalArgumentException("文件路径格式不正确: " + e.getMessage());
        }
    }

    /**
     * 验证文件名安全性
     */
    private static String validateFileName(String fileName) {
        logger.error("原始文件名: {}", fileName);
        if (fileName == null || fileName.trim().isEmpty()) {
            throw new IllegalArgumentException("文件名不能为空");
        }

        // 长度检查
        if (fileName.length() > MAX_FILENAME_LENGTH) {
            throw new IllegalArgumentException("文件名过长: " + fileName.length());
        }

        // 检查危险字符
        for (String dangerous : DANGEROUS_PATH_PATTERNS) {
            if (fileName.contains(dangerous)) {
                throw new IllegalArgumentException("文件名包含不安全字符: " + dangerous);
            }
        }

        // 检查文件名格式
        // if (!SAFE_FILENAME_PATTERN.matcher(fileName).matches()) {
        //     throw new IllegalArgumentException("文件名包含不安全字符");
        // }

        return fileName;
    }

    /**
     * 验证文件是否安全
     */
    private static void validateFileSecurely(File file) {
        if (file == null) {
            throw new IllegalArgumentException("文件对象不能为空");
        }

        try {
            // 获取规范化路径
            String canonicalPath = file.getCanonicalPath();

            // 检查文件大小
            if (file.exists() && file.isFile() && file.length() > MAX_FILE_SIZE) {
                throw new IllegalArgumentException("文件大小超过限制: " + file.length());
            }

            // 记录文件操作日志
            logger.debug("验证文件: " + canonicalPath);

        } catch (IOException e) {
            logger.error("文件验证失败: " + file.getPath(), e);
            throw new IllegalArgumentException("文件验证失败: " + e.getMessage());
        }
    }

    /**
     * 安全创建文件对象
     */
    private static File createSecureFile(String path) {
        String safePath = validateAndSanitizePath(path);
        File file = new File(safePath);
        validateFileSecurely(file);
        return file;
    }

    /**
     * 安全创建文件对象（父路径+子路径）
     */
    private static File createSecureFile(String parentPath, String childPath) {
        String safeParentPath = validateAndSanitizePath(parentPath);
        String safeChildPath = validateFileName(childPath);

        // 使用安全的路径拼接
        Path safePath = Paths.get(safeParentPath, safeChildPath);
        File file = new File(safePath.toString());
        validateFileSecurely(file);
        return file;
    }

    /**
     * 压缩成ZIP 方法1
     * @param srcDir 压缩文件夹路径
     * @param out    压缩文件输出流
     * @param KeepDirStructure  是否保留原来的目录结构,true:保留目录结构;
     * 							false:所有文件跑到压缩包根目录下(注意：不保留目录结构可能会出现同名文件,会压缩失败)
     * @throws RuntimeException 压缩失败会抛出运行时异常
     */
    public static void toZip(String srcDir, OutputStream out, boolean KeepDirStructure)
            throws RuntimeException{

        // 验证输入参数
        if (out == null) {
            throw new IllegalArgumentException("输出流不能为空");
        }

        long start = System.currentTimeMillis();
        ZipOutputStream zos = null ;
        try {
            // 使用安全的文件创建
            File sourceFile = createSecureFile(srcDir);

            zos = new ZipOutputStream(out);
            compress(sourceFile, zos, sourceFile.getName(), KeepDirStructure);

            long end = System.currentTimeMillis();
            logger.info("压缩完成，耗时：" + (end - start) + " ms");

        } catch (Exception e) {
            logger.error("ZIP压缩失败", e);
            throw new RuntimeException("zip error from ZipUtils", e);
        } finally {
            if(zos != null){
                try {
                    zos.close();
                } catch (IOException e) {
                    logger.error("关闭ZIP输出流失败", e);
                }
            }
        }
    }

    /**
     * 压缩成ZIP 方法2
     * @param srcFiles 需要压缩的文件列表
     * @param out 	        压缩文件输出流
     * @throws RuntimeException 压缩失败会抛出运行时异常
     */
    public static void toZip(List<File> srcFiles , OutputStream out , Map map)throws RuntimeException {
        // 验证输入参数
        if (srcFiles == null || srcFiles.isEmpty()) {
            throw new IllegalArgumentException("源文件列表不能为空");
        }
        if (out == null) {
            throw new IllegalArgumentException("输出流不能为空");
        }
        if (map == null) {
            throw new IllegalArgumentException("文件名映射不能为空");
        }

        long start = System.currentTimeMillis();
        ZipOutputStream zos = null ;
        try {
            zos = new ZipOutputStream(out);
            zos.setEncoding("UTF-8");

            for (File srcFile : srcFiles) {
                // 验证源文件安全性
                validateFileSecurely(srcFile);

                byte[] buf = new byte[BUFFER_SIZE];
                String zipEntryName = validateFileName(map.get(srcFile.getName()) + "");
                zos.putNextEntry(new ZipEntry(zipEntryName));

                int len;
                FileInputStream in = new FileInputStream(srcFile);
                try {
                    while ((len = in.read(buf)) != -1){
                        zos.write(buf, 0, len);
                    }
                } finally {
                    in.close();
                }
                zos.closeEntry();
            }

            long end = System.currentTimeMillis();
            logger.info("压缩完成，耗时：" + (end - start) + " ms");

        } catch (Exception e) {
            logger.error("ZIP压缩失败", e);
            throw new RuntimeException("zip error from ZipUtils", e);
        } finally {
            if(zos != null){
                try {
                    zos.close();
                } catch (IOException e) {
                    logger.error("关闭ZIP输出流失败", e);
                }
            }
        }
    }

    /**
     * 递归压缩方法
     * @param sourceFile 源文件
     * @param zos		 zip输出流
     * @param name		 压缩后的名称
     * @param KeepDirStructure  是否保留原来的目录结构,true:保留目录结构;
     * 							false:所有文件跑到压缩包根目录下(注意：不保留目录结构可能会出现同名文件,会压缩失败)
     * @throws Exception
     */
    private static void compress(File sourceFile, ZipOutputStream zos, String name,
                                 boolean KeepDirStructure) throws Exception{
        // 验证参数
        validateFileSecurely(sourceFile);
        String safeName = validateFileName(name);

        byte[] buf = new byte[BUFFER_SIZE];
        if(sourceFile.isFile()){
            // 向zip输出流中添加一个zip实体，构造器中name为zip实体的文件的名字
            zos.setEncoding("gbk");
            logger.debug("压缩文件: " + safeName);
            zos.putNextEntry(new ZipEntry(safeName));

            // copy文件到zip输出流中
            int len;
            FileInputStream in = new FileInputStream(sourceFile);
            try {
                while ((len = in.read(buf)) != -1){
                    zos.write(buf, 0, len);
                }
            } finally {
                in.close();
            }
            // Complete the entry
            zos.closeEntry();
        } else {
            File[] listFiles = sourceFile.listFiles();
            if(listFiles == null || listFiles.length == 0){
                // 需要保留原来的文件结构时,需要对空文件夹进行处理
                if(KeepDirStructure){
                    // 空文件夹的处理
                    zos.putNextEntry(new ZipEntry(safeName + "/"));
                    // 没有文件，不需要文件的copy
                    zos.closeEntry();
                }
            } else {
                for (File file : listFiles) {
                    String fileName = validateFileName(file.getName());
                    // 判断是否需要保留原来的文件结构
                    if (KeepDirStructure) {
                        // 注意：file.getName()前面需要带上父文件夹的名字加一斜杠,
                        // 不然最后压缩包中就不能保留原来的文件结构,即：所有文件都跑到压缩包根目录下了
                        compress(file, zos, safeName + "/" + fileName, KeepDirStructure);
                    } else {
                        compress(file, zos, fileName, KeepDirStructure);
                    }
                }
            }
        }
    }

    //创建文件夹 - 安全修复版本
    public static void createFolder(String destPath){
        try {
            // 修复第151行：使用安全的文件创建方法
            File file = createSecureFile(destPath);
            File parent = file.getParentFile();

            logger.debug("创建文件夹: " + file.getCanonicalPath());

            // 如果保存路径不存在，则创建路径
            if (!parent.exists()) {
                boolean created = parent.mkdirs();
                if (!created) {
                    throw new RuntimeException("创建目录失败: " + parent.getCanonicalPath());
                }
                logger.info("成功创建目录: " + parent.getCanonicalPath());
            }
        } catch (Exception e) {
            logger.error("创建文件夹失败: " + destPath, e);
            throw new RuntimeException("创建文件夹失败: " + e.getMessage(), e);
        }
    }

    public static void deleteFile(String localname) {
        try {
            // 验证输入参数
            if (localname == null || localname.trim().isEmpty()) {
                throw new IllegalArgumentException("删除路径不能为空");
            }

            logger.info("删除文件路径: " + localname);

            // 确保路径以斜杠结尾
            if(!"/".equals(localname.substring(localname.length()-1,localname.length()))){
                localname = localname + "/";
            }

            // 修复第165行：使用安全的文件创建方法
            File file = createSecureFile(localname);

            if (file.isDirectory()) {
                String[] filelist = file.list();
                if (filelist != null) {
                    for (String fileName : filelist) {
                        try {
                            // 修复路径拼接安全问题：使用安全的文件创建方法
                            File delfile = createSecureFile(localname, fileName);

                            if (!delfile.isDirectory()) {
                                boolean deleted = delfile.delete();
                                if (deleted) {
                                    logger.debug("成功删除文件: " + delfile.getCanonicalPath());
                                } else {
                                    logger.warn("删除文件失败: " + delfile.getCanonicalPath());
                                }
                            }
                        } catch (Exception e) {
                            logger.error("删除文件失败: " + fileName, e);
                            // 继续删除其他文件，不中断整个删除过程
                        }
                    }
                }
            } else {
                logger.warn("指定路径不是目录: " + localname);
            }
        } catch (Exception e) {
            logger.error("删除文件操作失败: " + localname, e);
            throw new RuntimeException("删除文件失败: " + e.getMessage(), e);
        }
    }

    public static void main(String[] args) throws Exception {
        /** 测试压缩方法1  */
//        FileOutputStream fos1 = new FileOutputStream(new File("c:/mytest01.zip"));
//        ZipUtils.toZip("D:/log", fos1,true);

        /** 测试压缩方法2  */
//        List<File> fileList = new ArrayList<>();
//        fileList.add(new File("C:/GW_714321_202322.pdf"));
//        FileOutputStream fos2 = new FileOutputStream(new File("C:/mytest02.zip"));
//        Map map=new HashMap();
//        map.put("GW_714321_202322.pdf","辽宁省气象局关于再次征求《辽宁省贯彻落实'气象高质量发展纲要（2022-2035年）'行动方案（征求意见稿）》意见的函公文处理单.pdf");
//        ZipUtils.toZip(fileList, fos2, map);

        long currentTimeMillis = System.currentTimeMillis();
        logger.info("当前时间戳: " + currentTimeMillis);
    }
}
